﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using MentalAlchemy.Atomics;
using MentalAlchemy.Molecules;
using MentalAlchemy.Molecules.Image;
using MentalAlchemy.Molecules.MachineLearning;

namespace OrthoEAImage
{
	public partial class MainForm : Form
	{
		private List<TrainingSample> trainData;

		public MainForm()
		{
			InitializeComponent();

			openFileDialog1.Filter = Const.IMAGE_FILE_FILTER;

			ActivationCombo.Items.AddRange(ActivationFunctions.Functions());
			ActivationCombo.SelectedIndex = 0;
			FitnessCombo.Items.AddRange(CombinedAnn.FitnessFunctions.GetFitnessFunctions());
			FitnessCombo.SelectedIndex = 0;
		}

		private void StartBtn_Click(object sender, EventArgs e)
		{
			//
			// Create training data.
			trainData = CreateTrainingData(foldersListControl1.Folders);

			//
			// Create EA.
			//var ne = new CombinedAnn();
			//ne.Ann1FitnessFunction = CombinedAnn.FitnessFunctions.GetFitnessFunction((string)FitnessCombo.SelectedItem);
			//ne.Ann1FitnessFunction.TrainData = trainData;

			//
			// Create ANN.
			var props = new NeuralNetProperties();
			props.nodesNumber = new[] { trainData[0].Data.Length, (int)OutputsNumeric.Value };
			props.actFunctions = new [] { ActivationFunctions.Identity, ActivationFunctions.GetActivationFunction((string) ActivationCombo.SelectedItem)};
			var ann = LayeredNeuralNetwork.CreateNetwork(props);

			//
			// Create NE algorithm.
			var ne = new NeuroEvolutionaryAlgorithm();
			var eaParams = eaPropertiesControl1.Parameters;
			eaParams.MinGeneValue = -0.1f;
			eaParams.GeneValueRange = 0.2f;
			eaParams.IndividualSize = ann.GetTotalConnectionsNumber();
			eaParams.MRate = 1f / eaParams.IndividualSize;

			//
			// Prepare fitness function.
			var fitnessFunc = CombinedAnn.FitnessFunctions.GetFitnessFunction((string)FitnessCombo.SelectedItem);
			fitnessFunc.Network = ann;
			fitnessFunc.TrainData = trainData;

			FitnessComparator.MinimizeFitness = fitnessFunc.MinimizeFitness;
			ne.FitnessFunction = fitnessFunc.ObjectiveFunction;
			ne.NeuralNetwork = ann;
			ne.Init(eaParams);

			//
			// Run algorithm iteratively with stopping after each 'Stage Duration' generations.
			var stages = eaParams.GenerationsNumber / (int)StageNumeric.Value;
			eaParams.GenerationsNumber = (int)StageNumeric.Value;
			var stats = new List<Stats>();
			#region - Save ANN properties right after initialization. -
			//
			// Save resulting weights of nodes.
			var idx = "0";
			var DEF_FOLDER = "Weights g" + idx;
			SaveAnnWeights(ann, DEF_FOLDER);

			//
			// Save ANN responses.
			string RESPS_FILE = Path.Combine(Application.StartupPath, DEF_FOLDER);
			RESPS_FILE = Path.Combine(RESPS_FILE, "ann.log");
			SaveAnnResponse(ann, trainData, RESPS_FILE); 
			#endregion

			for (var i = 0; i < stages; ++i)
			{
				stats = CombinedAnnElements.ContinueTrainAnn1(trainData, ne, eaParams, fitnessFunc, ann);

				//
				// Save resulting weights of nodes.
				idx = ((i+1)*StageNumeric.Value).ToString();
				DEF_FOLDER = "Weights g" + idx;
				SaveAnnWeights(ann, DEF_FOLDER);

				//
				// Save ANN responses.
				RESPS_FILE = Path.Combine(Application.StartupPath, DEF_FOLDER);
				RESPS_FILE = Path.Combine(RESPS_FILE, "ann.log");
				SaveAnnResponse(ann, trainData, RESPS_FILE);
			}
			
			using (var writer = new StreamWriter("ne.log"))
			{
				EAElements.Write(writer, eaPropertiesControl1.Parameters);
				writer.WriteLine();
				writer.WriteLine("Fitness function:\t" + FitnessCombo.SelectedItem);
				writer.WriteLine("Activation function:\t" + ActivationCombo.SelectedItem);
				writer.WriteLine("ANN1 outputs number:\t" + OutputsNumeric.Value);
				writer.WriteLine("Stage duration:\t" + StageNumeric.Value);
				writer.WriteLine();
				FileIO.WriteStatsNumerate(writer, stats, true);
			}
		}

		private void SaveAnnResponse(LayeredNeuralNetwork ann, List<TrainingSample> data, string file)
		{
			string lineFormat = "{0}:\t{1}";
			var lines = new List<string> ();
			var outs = new List<List<float>>();
			lines.Add("ANN outputs:");
			foreach (var sample in trainData)
			{
				var inputs = MatrixMath.ConvertToVector(sample.Data);
				ann.Calculate(inputs);

				var outputs = new List<float>();
				ann.GetOutputs(out outputs);
				var outStr = VectorMath.ConvertToString(outputs.ToArray(), '\t');
				lines.Add(string.Format(lineFormat, sample.Name, outStr));

				outs.Add(outputs);
			}

			//
			// Perform normalization.
			lines.Add("");
			lines.Add("Normalized outputs:");
			for (int i1 = 0; i1 < outs.Count; i1++)
			{
				var output = outs[i1];
				var max = VectorMath.Max(output.ToArray());
				var min = VectorMath.Min(output.ToArray());
				var delta = max - min;
				if (delta == 0) continue;

				for (int i = 0; i < output.Count; i++)
				{
					output[i] = (output[i] - min)/delta;
				}
				var outStr = VectorMath.ConvertToString(output.ToArray(), '\t');
				lines.Add(string.Format(lineFormat, data[i1].Name, outStr));
			}

			//
			// Compute Gram matrix for ANN weights.
			lines.Add("");
			lines.Add("Gram matrix for output nodes' weights:");
			var w = ann.Layers[1].GetWeights();
			var gram = MatrixMath.ComputeGramMatrix(w);
			var rows = MatrixMath.ConvertToRowsStringsList(gram, '\t');
			lines.AddRange(rows);

			File.WriteAllLines(file, lines.ToArray());
		}

		/// <summary>
		/// Saves weights of the given ANN as images.
		/// </summary>
		private static void SaveAnnWeights (LayeredNeuralNetwork ann, string folder)
		{
			var path = Path.Combine(Application.StartupPath, folder);
			if (!Directory.Exists(path)) {Directory.CreateDirectory(path); }

			var sum = new float[ann.Layers[1].Nodes[0].Weights.Count];
			for (int i = 0; i < ann.OutputsNumber; i++)
			{
				//
				// use weights from the 1st layer.
				// Important! The images are supposed to be square!
				var w = ann.Layers[1].Nodes[i].Weights;
				var cols = (int)Math.Sqrt(w.Count);
				var m = MatrixMath.CreateFromVector(w.ToArray(), cols);

				var img = ImageConversion.CreateBmpFromGrayscaleTableNorm(m);
				var filename = string.Format("weights_{0}.png", (i+1));
				img.Save(Path.Combine(path, filename));

				VectorMath.Accumulate(ref sum, w.ToArray());
			}

			var sumCols = (int)Math.Sqrt(sum.Length);
			var sumM = MatrixMath.CreateFromVector(sum.ToArray(), sumCols);

			var sumImg = ImageConversion.CreateBmpFromGrayscaleTableNorm(sumM);
			var sumFilename = string.Format("sum_weights.png");
			sumImg.Save(Path.Combine(path, sumFilename));
		}

		private static List<TrainingSample> CreateTrainingData (string[] folders)
		{
			var res = new List<TrainingSample>();
			foreach (var folder in folders)
			{
				res.AddRange(GetTrainingDataFromFolder(folder));
			}

			return res;
		}

		private static List<TrainingSample> GetTrainingDataFromFolder (string folder)
		{
			var res = new List<TrainingSample>();

			var files = Directory.GetFiles(folder);
			foreach (var file in files)
			{
				if (!Generic.IsImageFile(file)) continue;

				var tempImg = new Bitmap(file);
				var pixels = ImageConversion.ConvertToGrayscaleTableFloat(tempImg);
				var pixelsV = MatrixMath.ConvertToVector(pixels);
				pixels = MatrixMath.CreateFromVector(pixelsV, pixelsV.Length);
				MatrixMath.NormalizeSelf(ref pixels);
				var tSample = new TrainingSample();
				tSample.Data = pixels;
				tSample.ClassID = 0;
				tSample.Name = file;

				res.Add(tSample);
			}
			return res;
		}
	}
}
